// FqlDataSource.cs
// Facebook/FrameworkWeb/Web
// Copyright (c) 2007, Nikhil Kothari. All Rights Reserved.
//

using System;
using System.Collections;
using System.Collections.Specialized;
using System.ComponentModel;
using System.ComponentModel.Design;
using System.Diagnostics;
using System.Drawing;
using System.Drawing.Design;
using System.Text;
using System.Web;
using System.Web.UI;
using System.Web.UI.Design;
using System.Web.UI.Design.WebControls;
using System.Web.UI.WebControls;
using Facebook.Service;
using Facebook.Service.Core;

namespace Facebook.Web {

    /// <summary>
    /// Encapsulates Facebook's FQL-based queries and the fql.query service APIs
    /// into an ASP.NET datasource control.
    /// </summary>
    [
    DefaultProperty("FqlQuery"),
    Designer(typeof(Facebook.Design.Web.FqlDataSourceDesigner), typeof(IDesigner)),
    Description("Allows you to query Facebook data using FQL"),
    DisplayName("FQL DataSource"),
    ParseChildren(true),
    PersistChildren(false),
    ToolboxBitmap(typeof(FqlDataSource))
    ]
    public class FqlDataSource : DataSourceControl {

        internal const string FqlViewName = "FqlResults";

        private string _fqlQuery;
        private ParameterCollection _parameters;
        private string _sortField;
        private SortDirection _sortDirection;

        private FqlResultsDataSourceView _fqlResultsView;

        /// <summary>
        /// The FQL query to execute.
        /// </summary>
        [
        Category("Data"),
        DefaultValue(""),
        Description("The FQL query to use to retrieve results from Facebook."),
        MergableProperty(false),
        ]
        public string FqlQuery {
            get {
                if (String.IsNullOrEmpty(_fqlQuery)) {
                    return String.Empty;
                }
                return _fqlQuery;
            }
            set {
                _fqlQuery = value;
                RaiseDataSourceChangedEvent(EventArgs.Empty);
            }
        }

        private FqlResultsDataSourceView FqlResultsView {
            get {
                if (_fqlResultsView == null) {
                    _fqlResultsView = new FqlResultsDataSourceView(this);
                }
                return _fqlResultsView;
            }
        }

        /// <summary>
        /// The set of named parameters to use to execute the FQL query.
        /// </summary>
        [
        Category("Data"),
        DefaultValue(null),
        Editor(typeof(ParameterCollectionEditor), typeof(UITypeEditor)),
        MergableProperty(false),
        PersistenceMode(PersistenceMode.InnerProperty)
        ]
        public ParameterCollection Parameters {
            get {
                if (_parameters == null) {
                    _parameters = new ParameterCollection();
                    _parameters.ParametersChanged += OnParametersChanged;
                    if (IsTrackingViewState) {
                        ((IStateManager)_parameters).TrackViewState();
                    }
                }
                return _parameters;
            }
        }

        private ParameterCollection ParametersInternal {
            get {
                return _parameters;
            }
        }

        /// <summary>
        /// Gets or sets the direction in which to sort the results.
        /// </summary>
        [
        Category("Data"),
        DefaultValue(SortDirection.Ascending),
        Description("The direction in which to sort the results")
        ]
        public SortDirection SortDirection {
            get {
                return _sortDirection;
            }
            set {
                if ((value < SortDirection.Ascending) || (value > SortDirection.Descending)) {
                    throw new ArgumentOutOfRangeException("value");
                }
                _sortDirection = value;
            }
        }

        /// <summary>
        /// Gets or sets the name of the field on which to sort the results.
        /// </summary>
        [
        Category("Data"),
        DefaultValue(""),
        Description("The name of the field on which to sort the results.")
        ]
        public string SortField {
            get {
                if (String.IsNullOrEmpty(_sortField)) {
                    return String.Empty;
                }
                return _sortField;
            }
            set {
                _sortField = value;
            }
        }

        /// <summary>
        /// Gets the specified data source view that can be used to retrieve
        /// data from this data source.
        /// </summary>
        /// <param name="viewName">The name of the view.</param>
        /// <returns>The appropriate DataSourceView instance.</returns>
        protected override DataSourceView GetView(string viewName) {
            if ((String.IsNullOrEmpty(viewName) == false) &&
                (String.Compare(viewName, FqlViewName, StringComparison.OrdinalIgnoreCase) != 0)) {
                throw new ArgumentOutOfRangeException("viewName", "specified view was '" + viewName + "'");
            }

            return FqlResultsView;
        }

        /// <summary>
        /// Gets the list of available data source views surfaced by this data source.
        /// </summary>
        /// <returns>The list of names.</returns>
        protected override ICollection GetViewNames() {
            return new string[] { FqlViewName };
        }

        /// <internalonly />
        protected override void LoadViewState(object state) {
            object baseState = null;

            if (state != null) {
                Pair p = (Pair)state;
                baseState = p.First;

                if (p.Second != null) {
                    ((IStateManager)Parameters).LoadViewState(p.Second);
                }
            }
            base.LoadViewState(baseState);
        }

        /// <internalonly />
        protected override void OnInit(EventArgs e) {
            base.OnInit(e);

            if (DesignMode) {
                return;
            }

            Page.LoadComplete += new EventHandler(this.OnPageLoadComplete);
        }

        private void OnPageLoadComplete(object sender, EventArgs e) {
            if (_parameters != null) {
                _parameters.UpdateValues(Context, this);
            }
        }

        /// <internalonly />
        protected override void OnPreRender(EventArgs e) {
            FacebookApplication application = FacebookApplication.GetCurrent(Page);
            if (application != null) {
                throw new InvalidOperationException("You must add a FacebookApplication control to this page to use the FqlDataSource control.");
            }

            base.OnPreRender(e);
        }

        private void OnParametersChanged(object sender, EventArgs e) {
            FqlResultsView.RaiseChangedEvent();
        }

        /// <internalonly />
        protected override object SaveViewState() {
            object baseState = base.SaveViewState();
            object parameterState = null;

            if (_parameters != null) {
                parameterState = ((IStateManager)_parameters).SaveViewState();
            }

            if ((baseState != null) || (parameterState != null)) {
                return new Pair(baseState, parameterState);
            }
            return null;
        }

        /// <internalonly />
        protected override void TrackViewState() {
            base.TrackViewState();
            if (_parameters != null) {
                ((IStateManager)_parameters).TrackViewState();
            }
        }


        private sealed class FqlResultsDataSourceView : DataSourceView {

            private FqlDataSource _owner;

            public FqlResultsDataSourceView(FqlDataSource owner)
                : base(owner, FqlDataSource.FqlViewName) {
                _owner = owner;
            }

            public override bool CanSort {
                get {
                    return true;
                }
            }

            protected override IEnumerable ExecuteSelect(DataSourceSelectArguments arguments) {
                arguments.AddSupportedCapabilities(DataSourceCapabilities.Sort);
                arguments.RaiseUnsupportedCapabilitiesError(this);

                string sortField = _owner.SortField;
                SortDirection sortDirection = _owner.SortDirection;

                if (arguments.SortExpression.Length != 0) {
                    string[] sortExpressionParts = arguments.SortExpression.Split(' ');
                    bool validExpression = (sortExpressionParts.Length == 1) || (sortExpressionParts.Length == 2);

                    if (validExpression) {
                        sortField = sortExpressionParts[0];
                        sortDirection = SortDirection.Ascending;

                        if (sortExpressionParts.Length == 2) {
                            if (String.Compare(sortExpressionParts[1], "ASC", StringComparison.OrdinalIgnoreCase) == 0) {
                                sortDirection = SortDirection.Ascending;
                            }
                            else if (String.Compare(sortExpressionParts[1], "DESC", StringComparison.OrdinalIgnoreCase) == 0) {
                                sortDirection = SortDirection.Descending;
                            }
                            else {
                                validExpression = false;
                            }
                        }
                    }
                    if (validExpression == false) {
                        throw new ArgumentException("Invalid sort expression specified.");
                    }
                }

                FacebookApplication fbApp = FacebookApplication.GetCurrent(_owner.Page);
                Debug.Assert(fbApp != null);

                if (fbApp.IsSessionCreated == false) {
                    return null;
                }

                string query = GetFqlQuery();
                object result = fbApp.Service.Fql.ExecuteQuery(query);

                if (result == null) {
                    return null;
                }
                else if (result is IEnumerable) {
                    if (String.IsNullOrEmpty(sortField)) {
                        return (IEnumerable)result;
                    }
                    else {
                        Debug.Assert(result is ArrayList);

                        ((ArrayList)result).Sort(new ObjectComparer(sortField, sortDirection));
                        return (IEnumerable)result;
                    }
                }
                else {
                    return new object[] { result };
                }
            }

            private string GetFqlQuery() {
                string query = _owner.FqlQuery;
                if (query.Length == 0) {
                    return null;
                }

                ParameterCollection parameters = _owner.Parameters;
                if ((parameters != null) && (parameters.Count != 0)) {
                    StringBuilder queryBuilder = new StringBuilder(query);
                    IOrderedDictionary parameterTable = parameters.GetValues(_owner.Context, _owner);

                    foreach (DictionaryEntry entry in parameterTable) {
                        string name = "@" + entry.Key;
                        object value = entry.Value;

                        if (value != null) {
                            queryBuilder.Replace(name, value.ToString());
                        }
                    }

                    query = queryBuilder.ToString();
                }

                return query;
            }

            public void RaiseChangedEvent() {
                OnDataSourceViewChanged(EventArgs.Empty);
            }
        }


        private sealed class ObjectComparer : IComparer {

            private string _sortField;
            private SortDirection _sortDirection;

            internal ObjectComparer(string sortField, SortDirection sortDirection) {
                _sortField = sortField;
                _sortDirection = sortDirection;
            }

            int IComparer.Compare(object x, object y) {
                Debug.Assert(x is Hashtable);
                Debug.Assert(y is Hashtable);

                object field1 = ((Hashtable)x)[_sortField];
                object field2 = ((Hashtable)y)[_sortField];

                int result = Comparer.Default.Compare(field1, field2);
                return (_sortDirection == SortDirection.Ascending) ? result : -result;
            }
        }
    }
}
